package com.domesticmouse.waveoteditor.client;


import com.domesticmouse.waveoteditor.client.canvas.Canvas;
import com.domesticmouse.waveoteditor.client.canvas.Color;
import com.domesticmouse.waveoteditor.client.ops.GwtClientDocOp;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.AnnotationBoundaryMap;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.Attributes;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.BufferedDocInitialization;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.BufferedDocOp;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.DocInitializationCursor;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.algorithm.Composer;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.algorithm.DocOpInverter;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.impl.AttributesImpl;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.impl.DocInitializationBuilder;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.impl.DocOpBuffer;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.impl.DocOpBuilder;
import com.domesticmouse.waveoteditor.client.wave.model.document.operation.impl.DocOpUtil;
import com.domesticmouse.waveoteditor.client.wave.model.operation.OperationException;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;

public class ClientState {

	private static final String RECTANGLE_WIDTH = "rectangle:width";
	private static final String RECTANGLE_HEIGHT = "rectangle:height";
	private static final String RECTANGLE_Y = "rectangle:Y";
	private static final String RECTANGE_X = "rectange:X";
	private static final String LINE_END_Y = "line:endY";
	private static final String LINE_END_X = "line:endX";
	private static final String LINE_START_Y = "line:startY";
	private static final String LINE_START_X = "line:startX";
	private static final String CIRCLE_RADIUS = "circle:radius";
	private static final String CIRCLE_CENTER_Y = "circle:centerY";
	private static final String CIRCLE_CENTER_X = "circle:centerX";
	private static final String RECTANGLE = "rectangle";
	private static final String LINE = "line";
	private static final String CIRCLE = "circle";
	private BufferedDocInitialization document = new DocInitializationBuilder().build();
	private int serverRevision = 0;

	private ServerCommunicationStrategy serverCommunicationStrategy;

	private interface ServerCommunicationStrategy {
		void pollResponse(ServerResult serverResult);

		void pollExceptionResponse(Throwable t);

		void editUpdateResponse(ServerResult serverResult);

		void editUpdateExceptionResponse(Throwable t);

		void addEdit(BufferedDocOp edit);

		void idleTimeout();
	}

	private class IdlingCommunicationStrategy implements ServerCommunicationStrategy {

		private static final int ONE_SECOND = 1000;

		public IdlingCommunicationStrategy() {
			serverCommsDisplay.display("initiating idle poll");

			pollService.poll(serverRevision, new AsyncCallback<ServerResult>() {

				@Override
				public void onFailure(Throwable caught) {
					serverCommunicationStrategy.pollExceptionResponse(caught);
				}

				@Override
				public void onSuccess(ServerResult result) {
					serverCommunicationStrategy.pollResponse(result);
				}
			});

			serverCommsDisplay.display("idle poll initiated");
		}

		@Override
		public void addEdit(BufferedDocOp edit) {
			serverCommsDisplay.display("idle: edit added");

			try {
				document = DocOpUtil.asInitialization(Composer.compose(document, edit));
				renderCanvas();

				serverCommunicationStrategy = new EditSentCommunicationStrategy(edit);
			} catch (OperationException e) {
				Window.alert("Could not merge client side edit: " + e.toString());
				serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(e);
			}

		}

		@Override
		public void editUpdateExceptionResponse(Throwable t) {
			// this shouldn't happen
			Window.alert("editUpdateException received while in idle: " + t.toString());
			serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(t);
		}

		@Override
		public void pollExceptionResponse(Throwable t) {
			Window.alert("Idle poll failed: " + t.toString());
			serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(t);
		}

		@Override
		public void editUpdateResponse(ServerResult serverResult) {
			// this shouldn't happen
			Window.alert("editUpdate result received while in idle");
		}

		@Override
		public void pollResponse(ServerResult serverResult) {
			serverCommsDisplay.display("idle poll result received");

			serverRevision = serverResult.getServerRevision();
			try {
				BufferedDocOp bdo = serverResult.getClientDocOp().buildBufferedDocOp();
				BufferedDocOp composedDocument = Composer.compose(document, bdo);
				document = DocOpUtil.asInitialization(composedDocument);
				serverRevision = serverResult.getServerRevision();
				renderCanvas();

				Timer timer = new Timer() {

					@Override
					public void run() {
						serverCommunicationStrategy.idleTimeout();
					}

				};

				timer.schedule(ONE_SECOND);

				serverCommsDisplay.display("idle timer set");

			} catch (OperationException e) {
				Window.alert("server document didn't compose: " + e.toString());
				serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(e);
			}

		}

		@Override
		public void idleTimeout() {
			serverCommsDisplay.display("idle timer fired");

			serverCommunicationStrategy = new IdlingCommunicationStrategy();
		}

	}

	private class EditSentCommunicationStrategy implements ServerCommunicationStrategy {

		public EditSentCommunicationStrategy(BufferedDocOp initiatingEdit) {
			serverCommsDisplay.display("sending edit update");

			GwtClientDocOp edit = new GwtClientDocOp();
			edit.configureFromBufferedDocOp(initiatingEdit);
			updateService.update(edit, serverRevision, new AsyncCallback<ServerResult>() {

				@Override
				public void onFailure(Throwable caught) {
					serverCommunicationStrategy.editUpdateExceptionResponse(caught);
				}

				@Override
				public void onSuccess(ServerResult result) {
					serverCommunicationStrategy.editUpdateResponse(result);
				}
			});
		}

		@Override
		public void addEdit(BufferedDocOp edit) {
			serverCommsDisplay.display("editSent: edit added");

			try {
				document = DocOpUtil.asInitialization(Composer.compose(document, edit));
				renderCanvas();

				serverCommunicationStrategy = new EditsAfterEditSentCommunicationStrategy(edit);
			} catch (OperationException e) {
				Window.alert("Could not merge client side edit: " + e.toString());
				serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(e);
			}

		}

		@Override
		public void editUpdateExceptionResponse(Throwable t) {
			Window.alert("Exception caught attempting to post edit: " + t.toString());
			serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(t);
		}

		@Override
		public void pollExceptionResponse(Throwable t) {
			Window.alert("Exception caught attempting to poll: " + t.toString());
			serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(t);
		}

		@Override
		public void editUpdateResponse(ServerResult serverResult) {
			serverCommsDisplay.display("update response received");

			serverRevision = serverResult.getServerRevision();
			try {
				BufferedDocOp bdo = serverResult.getClientDocOp().buildBufferedDocOp();
				BufferedDocOp composedDocument = Composer.compose(document, bdo);
				document = DocOpUtil.asInitialization(composedDocument);
				serverRevision = serverResult.getServerRevision();
				renderCanvas();
				serverCommunicationStrategy = new IdlingCommunicationStrategy();
			} catch (OperationException e) {
				Window.alert("Could not compose server result: " + e.toString());
				serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(e);
			}
		}

		@Override
		public void pollResponse(ServerResult serverResult) {
			// Ignore. Integrating poll response would make the update result
			// unusable.
		}

		@Override
		public void idleTimeout() {
			// Ignore.
		}

	}

	private class EditsAfterEditSentCommunicationStrategy implements ServerCommunicationStrategy {

		private BufferedDocOp editBuffer;

		public EditsAfterEditSentCommunicationStrategy(BufferedDocOp edit) {
			editBuffer = edit;
		}

		@Override
		public void addEdit(BufferedDocOp edit) {
			serverCommsDisplay.display("editsAfterEditSent: add edit");

			try {
				editBuffer = Composer.compose(editBuffer, edit);
				document = DocOpUtil.asInitialization(Composer.compose(document, edit));
				renderCanvas();
			} catch (OperationException e) {
				Window.alert("Could not compose edit: " + e.toString());
				serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(e);
			}
		}

		@Override
		public void editUpdateExceptionResponse(Throwable t) {
			Window.alert("Exception caught attempting to post edit: " + t.toString());
			serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(t);
		}

		@Override
		public void pollExceptionResponse(Throwable t) {
			Window.alert("Exception caught attempting to poll: " + t.toString());
			serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(t);
		}

		@Override
		public void editUpdateResponse(ServerResult serverResult) {
			serverCommsDisplay.display("editsAfterEditSent: update response received");

			serverRevision = serverResult.getServerRevision();
			try {
				document = DocOpUtil.asInitialization(Composer.compose(document, serverResult.getClientDocOp().buildBufferedDocOp()));
				serverRevision = serverResult.getServerRevision();
				renderCanvas();
				serverCommunicationStrategy = new EditSentCommunicationStrategy(editBuffer);
			} catch (OperationException e) {
				Window.alert("Could not compose server result: " + e.toString());
				serverCommunicationStrategy = new ServerSyncLostServerCommunicationStrategy(e);
			}
		}

		@Override
		public void idleTimeout() {
			// Ignore
		}

		@Override
		public void pollResponse(ServerResult serverResult) {
			// Ignore. Integrating a poll response would make the update
			// response hard to integrate.
		}

	}

	private class ServerSyncLostServerCommunicationStrategy implements ServerCommunicationStrategy {

		public ServerSyncLostServerCommunicationStrategy(Throwable t) {
			t.printStackTrace(System.out);
			serverCommsDisplay.display("Server Sync Lost: Please refresh browser: "+t);
		}

		@Override
		public void addEdit(BufferedDocOp edit) {
			Window.alert("Server Sync Lost: Please refresh browser");
		}

		@Override
		public void editUpdateExceptionResponse(Throwable t) {
			// Ignore
		}

		@Override
		public void editUpdateResponse(ServerResult serverResult) {
			// Ignore
		}

		@Override
		public void idleTimeout() {
			// Ignore
		}

		@Override
		public void pollExceptionResponse(Throwable t) {
			// Ignore
		}

		@Override
		public void pollResponse(ServerResult serverResult) {
			// Ignore
		}

	}

	// client edit state machine. tool selected, points/key strokes/etc, end
	// tool selected. part selected, edit part.

	private CanvasStateStrategy canvasStateStrategy;

	private interface CanvasStateStrategy {

		void canvasClicked(double x, double y);

		void render();

	}

	private class NoToolSelectedCanvasStateStrategy implements CanvasStateStrategy {

		public NoToolSelectedCanvasStateStrategy() {
			hintDisplay.display("Please select a Tool Button (above).");
		}

		@Override
		public void canvasClicked(double x, double y) {
			// TODO Implement the ability to select an item on the canvas.
		}

		@Override
		public void render() {
			// Nothing to render.
		}

	}

	private class RectangleToolCanvasStateStrategy implements CanvasStateStrategy {

		public RectangleToolCanvasStateStrategy() {
			hintDisplay.display("Click on the canvas to select where to start your rectangle");
		}

		@Override
		public void canvasClicked(double x, double y) {
			canvasStateStrategy = new RectanglePointSelectedCanvasStateStrategy(x, y);
		}

		@Override
		public void render() {
			// nothing to display
		}

	}

	private class RectanglePointSelectedCanvasStateStrategy implements CanvasStateStrategy {

		private double startX;
		private double startY;

		public RectanglePointSelectedCanvasStateStrategy(double x, double y) {
			this.startX = x;
			this.startY = y;
			hintDisplay.display("Click on the canvas to size your rectange");
		}

		@Override
		public void canvasClicked(double endX, double endY) {
			double left = Math.min(this.startX, endX);
			double right = Math.max(this.startX, endX);
			double top = Math.min(this.startY, endY);
			double bottom = Math.max(this.startY, endY);

			int docLength = docLength();
			DocOpBuilder docOpBuilder = new DocOpBuilder();
			if (docLength>0)
				docOpBuilder.retain(docLength);
			BufferedDocOp edit = docOpBuilder.elementStart(
					RECTANGLE,
					new AttributesImpl(RECTANGE_X, Double.toString(left), RECTANGLE_Y, Double.toString(top), RECTANGLE_WIDTH, Double.toString(right
							- left), RECTANGLE_HEIGHT, Double.toString(bottom - top))).elementEnd().build();
			serverCommunicationStrategy.addEdit(edit);
			canvasStateStrategy = new NoToolSelectedCanvasStateStrategy();
		}

		@Override
		public void render() {
			canvas.setStrokeStyle(Color.RED);
			canvas.strokeRect(startX - 2.5, startY - 2.5, 5, 5);
			canvas.closePath();
		}

	}

	private class LineCanvasStateStrategy implements CanvasStateStrategy {

		public LineCanvasStateStrategy() {
			hintDisplay.display("Click on the canvas to select the start of your line.");
		}

		@Override
		public void canvasClicked(double x, double y) {
			canvasStateStrategy = new LineStartSelectedCanvasStateStrategy(x, y);
		}

		@Override
		public void render() {
			// nothing to render
		}

	}

	private class LineStartSelectedCanvasStateStrategy implements CanvasStateStrategy {

		private double startX;
		private double startY;

		public LineStartSelectedCanvasStateStrategy(double x, double y) {
			this.startX = x;
			this.startY = y;
			hintDisplay.display("Click on the canvas to select the end of your line.");
		}

		@Override
		public void canvasClicked(double endX, double endY) {
			int docLength = docLength();
			DocOpBuilder docOpBuilder = new DocOpBuilder();
			if (docLength>0)
				docOpBuilder.retain(docLength);
			BufferedDocOp edit = docOpBuilder.elementStart(
					LINE,
					new AttributesImpl(LINE_START_X, Double.toString(startX), LINE_START_Y, Double.toString(startY), LINE_END_X, Double
							.toString(endX), LINE_END_Y, Double.toString(endY))).elementEnd().build();
			serverCommunicationStrategy.addEdit(edit);
			canvasStateStrategy = new NoToolSelectedCanvasStateStrategy();
		}

		@Override
		public void render() {
			canvas.setStrokeStyle(Color.RED);
			canvas.strokeRect(startX - 2.5, startY - 2.5, 5, 5);
			canvas.closePath();
		}

	}

	private class CircleToolCanvasStateStrategy implements CanvasStateStrategy {

		public CircleToolCanvasStateStrategy() {
			hintDisplay.display("Click on the canvas to select the center of your circle.");
		}

		@Override
		public void canvasClicked(double x, double y) {
			canvasStateStrategy = new CircleCenterSelectedCanvasStateStrategy(x, y);
		}

		@Override
		public void render() {
			// nothing to render
		}

	}

	private class CircleCenterSelectedCanvasStateStrategy implements CanvasStateStrategy {

		private double centerX;
		private double centerY;

		public CircleCenterSelectedCanvasStateStrategy(double x, double y) {
			this.centerX = x;
			this.centerY = y;
			hintDisplay.display("Click on the canvas to select the radius of your circle");
		}

		@Override
		public void canvasClicked(double radiusX, double radiusY) {
			double xdelta = this.centerX - radiusX;
			double ydelta = this.centerY - radiusY;
			double radius = Math.sqrt(xdelta * xdelta + ydelta * ydelta);

			int docLength = docLength();
			DocOpBuilder docOpBuilder = new DocOpBuilder();
			if (docLength>0)
				docOpBuilder.retain(docLength);
			BufferedDocOp edit = docOpBuilder.elementStart(
					CIRCLE,
					new AttributesImpl(CIRCLE_CENTER_X, Double.toString(this.centerX), CIRCLE_CENTER_Y, Double.toString(this.centerY), CIRCLE_RADIUS,
							Double.toString(radius))).elementEnd().build();
			serverCommunicationStrategy.addEdit(edit);
			canvasStateStrategy = new NoToolSelectedCanvasStateStrategy();
		}

		@Override
		public void render() {
			canvas.setStrokeStyle(Color.RED);
			canvas.strokeRect(centerX - 2.5, centerY - 2.5, 5, 5);
			canvas.closePath();
		}

	}

	public interface HintDisplay {
		void display(String hint);
	}

	public interface ServerCommsDisplay {
		void display(String serverComms);
	}

	private final Canvas canvas;
	private final HintDisplay hintDisplay;
	private final ServerCommsDisplay serverCommsDisplay;

	private final PollServiceAsync pollService = GWT.create(PollService.class);
	private final UpdateServiceAsync updateService = GWT.create(UpdateService.class);

	public ClientState(Canvas canvas, HintDisplay hintDisplay, ServerCommsDisplay serverCommsDisplay) {
		this.canvas = canvas;
		this.hintDisplay = hintDisplay;
		this.serverCommsDisplay = serverCommsDisplay;
		this.serverCommunicationStrategy = new IdlingCommunicationStrategy();
		this.canvasStateStrategy = new NoToolSelectedCanvasStateStrategy();
	}

	public void renderCanvas() {
		
		canvas.clear();

		document.apply(new DocInitializationCursor() {

			@Override
			public void elementStart(String type, Attributes attrs) {
				if (CIRCLE.equals(type)) {
					if (attrs.containsKey(CIRCLE_CENTER_X) && attrs.containsKey(CIRCLE_CENTER_Y) && attrs.containsKey(CIRCLE_RADIUS)) {
						canvas.beginPath();
						// TODO: wire up colour selector
						canvas.setFillStyle(Color.DARKGREEN);
						double x = Double.parseDouble(attrs.get(CIRCLE_CENTER_X));
						double y = Double.parseDouble(attrs.get(CIRCLE_CENTER_Y));
						double radius = Double.parseDouble(attrs.get(CIRCLE_RADIUS));
						canvas.arc(x, y, radius, 0, Math.PI * 2, true);
						canvas.closePath();
						canvas.fill();
					} else {
						hintDisplay.display("illegal circle in document: " + DocOpUtil.toConciseString(document));
					}
				} else if (LINE.equals(type)) {
					if (attrs.containsKey(LINE_START_X) && attrs.containsKey(LINE_START_Y) && attrs.containsKey(LINE_END_X)
							&& attrs.containsKey(LINE_END_Y)) {
						canvas.beginPath();
						canvas.setStrokeStyle(Color.BLUEVIOLET);
						double startX = Double.parseDouble(attrs.get(LINE_START_X));
						double startY = Double.parseDouble(attrs.get(LINE_START_Y));
						double endX = Double.parseDouble(attrs.get(LINE_END_X));
						double endY = Double.parseDouble(attrs.get(LINE_END_Y));
						canvas.moveTo(startX, startY);
						canvas.lineTo(endX, endY);
						canvas.closePath();
						canvas.stroke();
					} else {
						hintDisplay.display("illegal line in document: " + DocOpUtil.toConciseString(document));
					}
				} else if (RECTANGLE.equals(type)) {
					if (attrs.containsKey(RECTANGE_X) && attrs.containsKey(RECTANGLE_Y) && attrs.containsKey(RECTANGLE_HEIGHT)
							&& attrs.containsKey(RECTANGLE_WIDTH)) {
						canvas.beginPath();
						canvas.setFillStyle(Color.INDIAN_RED);
						double x = Double.parseDouble(attrs.get(RECTANGE_X));
						double y = Double.parseDouble(attrs.get(RECTANGLE_Y));
						double height = Double.parseDouble(attrs.get(RECTANGLE_HEIGHT));
						double width = Double.parseDouble(attrs.get(RECTANGLE_WIDTH));
						canvas.rect(x, y, width, height);
						canvas.fill();
					} else {
						hintDisplay.display("illegal rectange in document: " + DocOpUtil.toConciseString(document));
					}
				} else {
					hintDisplay.display("unknown type '" + type + "' in document: " + DocOpUtil.toConciseString(document));
				}

				canvasStateStrategy.render();
			}

			@Override
			public void elementEnd() {
				// Ignore
			}

			@Override
			public void characters(String chars) {
				// Ignore
			}

			@Override
			public void annotationBoundary(AnnotationBoundaryMap map) {
				// Ignore
			}
		});

		canvasStateStrategy.render();
	}

	public void onCanvasClick(int x, int y) {
		canvasStateStrategy.canvasClicked(x, y);
		renderCanvas();
	}

	public void onClearButtonSelected() {
		if (docLength() > 0) {
			// Clear the document by inverting it, and then applying the inverse
			// as an edit.
			DocOpInverter<BufferedDocOp> inverse = new DocOpInverter<BufferedDocOp>(new DocOpBuffer());
			document.apply(inverse);
			BufferedDocOp edit = inverse.finish();
			serverCommunicationStrategy.addEdit(edit);

			canvasStateStrategy = new NoToolSelectedCanvasStateStrategy();
		}
	}

	public void onLineButtonSelected() {
		canvasStateStrategy = new LineCanvasStateStrategy();
	}

	public void onRectangleButtonSelected() {
		canvasStateStrategy = new RectangleToolCanvasStateStrategy();
	}

	public void onCircleButtonSelected() {
		canvasStateStrategy = new CircleToolCanvasStateStrategy();
	}

	private int docLength() {
		final int i[] = { 0 };
		document.apply(new DocInitializationCursor() {

			@Override
			public void elementStart(String type, Attributes attrs) {
				i[0]++;
			}

			@Override
			public void elementEnd() {
				i[0]++;
			}

			@Override
			public void characters(String chars) {
				i[0] += chars.length();
			}

			@Override
			public void annotationBoundary(AnnotationBoundaryMap map) {
				i[0]++;
			}
		});
		return i[0];
	}

}
